package edu.arizona.sista.learn.model.groups;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import edu.arizona.sista.learn.model.Interval;
import edu.arizona.sista.learn.model.ParamInterval;

/**
 * A simple temporal model of a group.  A group has an interval and
 * knows about its grouping variations over time.
 * 
 * @author Anh Tran
 *
 */
public class Group implements Comparable<Group>{
	
	/**
	 * Sorted set of all grouping variations that took place
	 * during the lifetime of the group.
	 */
	private SortedSet<ParamInterval<GroupFormation>> _groupingVariations;
	
	private Interval _interval;
	private String _id;
	
	public Group(String id, int start, int end, Set<Person> members) {
		_id = id;
		_interval = new Interval(start, end);
		_groupingVariations = new TreeSet<ParamInterval<GroupFormation>>();
		_groupingVariations.add(new ParamInterval<GroupFormation>(start, end,
				new GroupFormation(members)));
	}

	public Interval interval() {
		return _interval;
	}
	
	public String id() {
		return _id;
	}
	
	/**
	 * Get the group's grouping information.
	 * @return The group's grouping information.
	 */
	public SortedSet<ParamInterval<GroupFormation>> groupingVariations() {
		return _groupingVariations;
	}
	
	/**
	 * Get the maximum number of people that actively participated in this
	 * group at any point in time.
	 * @return The maximum number of active participants over time.
	 */
	public int getMaxParticipants() {
		int max = 0;
		for (ParamInterval<GroupFormation> i : this.groupingVariations()) {
			max = Math.max(max, i.obj().size());
		}
		return max;
	}

	/**
	 * Get the minimum number of people that actively participated in this
	 * group at any point in time.
	 * @return The minimum number of active participants over time.
	 */
	public int getMinParticipants() {
		int min = 0;
		for (ParamInterval<GroupFormation> i : this.groupingVariations()) {
			if (min == 0)
				min = i.obj().size();
			else
				min = Math.max(min, i.obj().size());
		}
		return min;
	}
	
	/**
	 * Extend the ending time of the last grouping by the specified
	 * number of frames.
	 * @param frames Number of frames to extend.
	 */
	public void extendLastGroupingVariation(int frames) {
		this.groupingVariations().last().interval().setEnd(
				this.groupingVariations().last().interval().end()+frames);
		this.interval().setEnd(this.interval().end()+frames);
	}
	
	/**
	 * Get all participants that have been active within some
	 * time interval.
	 * @param i The interval.
	 * @return Set of all participants that participated
	 * in the group at one time or another within the given
	 * interval.
	 */
	public Set<Person> getActiveParticipants(Interval i) {
		Set<Person> participants = new HashSet<Person>();
		for (ParamInterval<GroupFormation> in : this.groupingVariations()) {
			if (i.overlaps(in.interval(), 0)) {
				participants.addAll(in.obj().participants());
			}
		}
		return participants;
	}
	
	/**
	 * Get all active participants at the given time step.
	 * @param timeStep The timestep.
	 * @return Set of active participants at the given time.
	 */
	public Set<Person> getActiveParticipants(int timeStep) {
		Set<Person> participants = new HashSet<Person>();
		for (ParamInterval<GroupFormation> in : this.groupingVariations()) {
			if (in.interval().isOn(timeStep)) {
				participants.addAll(in.obj().participants());
				break;
			}
		}
		return participants;
	}
	
	/**
	 * Merge another group to the end of this group (allowing some
	 * noise window).
	 * @param other Another group to be merged.
	 */
	public void mergeToEnd(Group other, int window) {
		// Let's make sure both groups are consistent before attempting to merge
		assert this.isConsistent() : "Group is not consistent.";
		assert other.isConsistent() : "Other group is not consistent.";
		
		// Make sure the groups are lined up end-to-end
		if ((this.interval().end() - window > other.interval().start())
				|| (this.interval().end() + window < other.interval().start())) {
			throw new RuntimeException("ERROR: Group " + this.id() + " "
					+ this.interval().toString() + " cannot mergeEnd Group "
					+ other.id() + " " + other.interval().toString() + ".");
		}
		
		// If a window was used to match the two groups, then let's
		// adjust our ending time to match the other group's starting time
		// to make sure the final groupings do not have any gaps
		ParamInterval<GroupFormation> last = this.groupingVariations().last();
		assert (last.interval().end() == this.interval().end())
			: "Last grouping's end ("+last.interval().end()+
				") does not match the group's end ("+this.interval().end()+").";
		last.interval().setEnd(other.interval().start());
		this.interval().setEnd(other.interval().start());
		
		// For the first grouping in the other group, we may consider
		// merging it with the last grouping of our group if they have
		// the same formation (i.e., contain the same people).
		ParamInterval<GroupFormation> firstOther = other.groupingVariations().first();
		if (last.obj().containsAll(firstOther.obj().participants()) &&
				firstOther.obj().containsAll(last.obj().participants())) {
			last.interval().setEnd(firstOther.interval().end());
			this.interval().setEnd(firstOther.interval().end());
			other.groupingVariations().remove(firstOther);
		}
		
		// Now that we've taken care of all the corner cases, complete the merge
		// by adding each grouping from the other group to this group.
		for (ParamInterval<GroupFormation> grouping : other.groupingVariations()) {
			this.groupingVariations().add(grouping);
			this.interval().setEnd(grouping.interval().end());
		}
		
		// Sanity check
		assert (this.interval().end() == other.interval().end())
		: "End of merged groups ("+this.interval().end()+") does not match the older's" +
			"group's end ("+other.interval().end()+").";
		assert this.isConsistent() : "Group is no longer consistent.";
	}
	
	/**
	 * Check the consistency of groupings to make sure there are no gaps.
	 * @return True of the groupings are consistent.
	 */
	public boolean isConsistent() {
		boolean pass = true;
		
		if (this.groupingVariations().first().interval().start() != this.interval().start()) {
			System.err.println("Group's start (" + this.interval().start() + ") " +
					"does not equal first grouping's start (" +
					this.groupingVariations().first().interval().start() + ").");
			pass = false;
		}
		
		if (this.groupingVariations().last().interval().end() != this.interval().end()) {
			System.err.println("Group's end (" + this.interval().end() + ") " +
					"does not equal first grouping's end (" +
					this.groupingVariations().first().interval().start() + ").");
			pass = false;
		}
		
		Iterator<ParamInterval<GroupFormation>> iterator = this.groupingVariations().iterator();
		ParamInterval<GroupFormation> previous = null, current = null;
		while (iterator.hasNext()) {
			current = iterator.next();
			if (previous != null) {
				if (previous.interval().end() != current.interval().start()) {
					System.err.println("Previous grouping's end (" + previous.interval().end() + ") " +
							"does not equal next grouping's start (" +
							current.interval().start() + ").");
					pass = false;
				}
				if (previous.obj().containsAll(current.obj().participants()) &&
						current.obj().containsAll(previous.obj().participants())) {
					System.out.println("Previous grouping contain the same members " +
							"as the next grouping (consider merging).");
				}
			}
			previous = current;
		}	
		
		return pass;
	}

	@Override
	public int compareTo(Group anotherGroup) {
		if (this == anotherGroup || this.id() == anotherGroup.id())
			return 0;
		
		// Compare using earliest starting first
		int comparison = Interval.esf.compare(this.interval(), anotherGroup.interval());
		if (comparison != 0)
			return comparison;
		
		if (this.getMaxParticipants() < anotherGroup.getMaxParticipants())
			return -1;
		if (this.getMaxParticipants() > anotherGroup.getMaxParticipants())
			return 1;
		
		return 0;
	}
	
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("Group " + this.id() + "\n");
		sb.append("Interval: " + this.interval().toString() + "\n");
		sb.append(this.groupingVariations().size() + " Grouping Variation(s):\n");
		for (ParamInterval<GroupFormation> grouping : this.groupingVariations()) {
			sb.append("- " + grouping.toString() + "\n");
		}
		
		return sb.toString();
	}
}
